#include <fcntl.h>
#include <pthread.h>
#include <rid.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <fn85.h>
#include <rid_fn85.h>

#include <nit/macros.h>
#include <nit/palloc.h>
#include <nit/list.h>
#include <nit/set.h>
#include <nit/map.h>

#include <oas.h>

#include "pum.h"
#include "global.h"

#define STRLEN(str) (sizeof(str) - 1)

typedef struct {
	Oas_sys *sys;
	Rid user_id;
        Rid_fn85 id_str;
	int fd;
	pthread_mutex_t death_mutex;
	int die;
	pthread_t repl_thread;
} User_dat;

static void
user_signal_death(User_dat *dat)
{
	pthread_mutex_lock(&dat->death_mutex);
	dat->die = 1;
	pthread_mutex_unlock(&dat->death_mutex);
}

static int
user_died(User_dat *dat)
{
	int died;

	pthread_mutex_lock(&dat->death_mutex);
	died = dat->die;
	pthread_mutex_unlock(&dat->death_mutex);
	return died;
}

static int
parse_id_w_error(Rid id, const char **str, const char *where)
{
	size_t len = 0;

	switch (rid_fn85_parse(id, *str, &len)) {
	case FN85_OKAY:
		*str += RID_FN85_LEN;
	        return 0;
	case FN85_INVALID:
		fprintf(stderr,
			"Something wasn't a digit at index %zu in "
			"the %s.\n",
			where,
			len);
		break;
	case FN85_TOO_HIGH:
		fprintf(stderr,
			"Number became larger than possible at index %zu "
			"in the %s.\n",
			where,
			len);
		break;
	}

	return -1;
}

static int
parse_space(const char **str, const char *where)
{
	if (**str != ' ') {
		fprintf(stderr, "Missing space after %s.\n", where);
		return -1;
	}

	++*str;
	return 0;
}

static int
parse_pum(const char *str, Rid to, Pum_val *val)
{
	Rid_fn85 pum_id_str;

	if (parse_id_w_error(to, &str, "receiver address") < 0)
		return -1;

	if (parse_space(&str, "receiver address") < 0)
		return -1;

	if (parse_id_w_error(val->indep, &str, "independent entity") < 0)
		return -1;

	if (parse_space(&str, "independent entity") < 0)
		return -1;

	if (!strncmp(str, "to", STRLEN("to"))) {
		val->type = PUM_TO;
		str += STRLEN("to");
	} else if (!strncmp(str, "what", STRLEN("what"))) {
		val->type = PUM_WHAT;
		str += STRLEN("what");
	} else {
		fprintf(stderr, "No or invalid pum type.\n");
		return -1;
	}

 	if (parse_space(&str, "pum type") < 0)
		return -1;

	if (parse_id_w_error(val->dep, &str, "dependent part") < 0)
		return -1;

	rid_set(val->id, NULL);
	rid_fn85(pum_id_str, val->id);
	printf("Pum val ID set to %s\n", pum_id_str);
	return 0;
}

static void *
repl(void *arg)
{
	User_dat *dat = arg;
	size_t str_len = 0;
	char *str = NULL;

	while (1) {
		Rid to;
		Pum_val *val;

		printf("[%s] ", dat->id_str);

		if (getline(&str, &str_len, stdin) < 0) {
			fprintf(stderr, "Error reading line.\n");
			continue;
		}

		if (!strncmp(str, "exit", STRLEN("exit")))
			break;

		if (!(val = global_get_pum(dat->sys))) {
			fprintf(stderr, "Error getting pum val.\n");
			continue;
		}

		if (parse_pum(str, to, val) < 0) {
			global_recycle_pum(dat->sys, val);
			continue;
		}

		if (oas_msg(dat->sys, dat->user_id, to, val) < 0) {
			fprintf(stderr, "Failed to send message.\n");
			global_recycle_pum(dat->sys, val);
			continue;
		}
	}

	free(str);
	user_signal_death(dat);
	pthread_exit(NULL);
}

static void
dprint_pum(int fd, const Rid sender, const Pum_val *val)
{
	Rid_fn85 str;

	rid_fn85(str, sender);
	dprintf(fd, "%s ", str);
	rid_fn85(str, val->indep);
	dprintf(fd, "%s ", str);

	if (val->type == PUM_WHAT)
		dprintf(fd, "what ");
	else
		dprintf(fd, "to ");

	rid_fn85(str, val->dep);
	dprintf(fd, "%s ", str);
	rid_fn85(str, val->id);
	dprintf(fd, "%s\n", str);
}

static int
user_act(Rid self, const Rid sender, void *msg, void *local, Oas_sys *sys)
{
	User_dat *dat = local;

	if (msg) {
		dprint_pum(dat->fd, sender, msg);
		global_recycle_pum(sys, msg);
	}

	if (user_died(dat))
		return 0;

	return 1;
}

static void
user_die(Rid self, Oas_mail *mail, void *local, Oas_sys *sys)
{
	User_dat *dat = local;

	pthread_join(dat->repl_thread, NULL);
	close(dat->fd);
	remove(dat->id_str);
	pthread_mutex_destroy(&dat->death_mutex);
	free(dat);
}

int
user_add(Oas_sys *sys, const Rid user_id)
{
	User_dat *dat = palloc(dat);

	if (!dat) {
		fprintf(stderr, "Failed to allocated user dat.\n");
		return -1;
	}

	dat->sys = sys;
	rid_set(dat->user_id, user_id);
	rid_fn85(dat->id_str, user_id);
	dat->die = 0;

	if (mkfifo(dat->id_str, 0666) < 0) {
		fprintf(stderr, "Failed to make fifo for user.\n");
		goto err_mkfifo;
	}

	if ((dat->fd = open(dat->id_str, O_WRONLY)) < 0) {
		fprintf(stderr, "Failed to open fifo for user.\n");
		goto err_open;
	}

	if (pthread_mutex_init(&dat->death_mutex, NULL)) {
		fprintf(stderr, "Failed to init user death mutex.\n");
		goto err_mutex;
	}

	if (oas_add(sys, user_id, NULL, dat, NULL, user_act, user_die) < 0) {
		fprintf(stderr, "Failed to add user actor.\n");
		goto err_add;
	}

	if (pthread_create(&dat->repl_thread, NULL, repl, dat)) {
		fprintf(stderr, "Failed starting repl thread.\n");
		user_signal_death(dat);
		return -1;
	}

	return 0;
err_add:
	pthread_mutex_destroy(&dat->death_mutex);
err_mutex:
	close(dat->fd);
err_open:
	remove(dat->id_str);
err_mkfifo:
	free(dat);
	return -1;
}
